/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.mios.internal;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.openhab.binding.mios.internal.config.DeviceBindingConfig;
import org.openhab.binding.mios.internal.config.MiosBindingConfig;
import org.openhab.binding.mios.internal.config.SceneBindingConfig;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpClientConfig.Builder;
import com.ning.http.client.Response;
import com.ning.http.client.providers.jdk.JDKAsyncHttpProvider;
/**
* Manages the HTTP Connection for a single MiOS Unit.
*
* This code has two functions to perform:
* <ul>
* <li>Inbound <i>changes</i> from the MiOS Unit.<br>
* Manages an internal thread to periodically poll the configured MiOS Unit and timeout at the specified interval. All
* changes are internally transformed from their original (JSON) form, and dispatched to the relevant parts of openHAB.
* <li>Outbound <i>commands</i> to the MiOS Unit.<br>
* Exposes a method to transform & transmit openHAB Commands to the configured MiOS Unit, in a non-blocking manner.<br>
* Callers wishing to utilize this functionality use the <code>invokeCommand</code> method.
* </ul>
*
* @author Mark Clark
* @since 1.6.0
*/
public class MiosUnitConnector {
private static final Logger logger = LoggerFactory.getLogger(MiosUnitConnector.class);
private static final String ENCODING_CHARSET = "utf-8";
private static final String BIND_COMMAND_VALUE = "??";
private static final String BIND_ITEM_INCREMENT = "?++";
private static final String BIND_ITEM_DECREMENT = "?--";
private static final String BIND_ITEM_VALUE = "?";
private static final String SCENE_URL = "http://%s:%d/data_request?id=action&serviceId=urn:micasaverde-com:serviceId:HomeAutomationGateway1&action=RunScene&SceneNum=%d";
private static final String DEVICE_URL = "http://%s:%d/data_request?id=action&DeviceNum=%d&serviceId=%s&action=%s";
private static final String DEVICE_URL_PARAMS = DEVICE_URL + "&%s";
private static final Pattern DEVICE_PATTERN = Pattern.compile("(?<serviceName>.+)/" + "(?<serviceAction>.+)"
+ "\\(((?<serviceParam>[a-zA-Z]+[a-zA-Z0-9]*)(=(?<serviceValue>.+))?)?\\)");
private static final Pattern ACTION_PATTERN = Pattern.compile("(?<serviceName>.+)/" + "(?<serviceAction>.+)");
// the MiOS instance and openHAB event publisher handles
private final MiosUnit unit;
private final MiosBinding binding;
private final AsyncHttpClient client;
private LongPoll pollCall;
private Thread pollThread;
boolean running;
// Create a place to keep the last "status" attribute, for each Device, so we can compare incoming values for
// Duplicates (Devices & Scenes)
//
// The MiOS system will send duplicate values, so we need this data to avoid pumelling openHAB with unnecessary
// changes (and it can only be done here due to the async nature of openHAB store writes). This map is keyed by the
// DeviceId/SceneId form the MiOS system, which can be either a String, or an Integer.
private HashMap<String, Integer> deviceStatusCache = new HashMap<String, Integer>(100);
private HashMap<Integer, Integer> sceneStatusCache = new HashMap<Integer, Integer>(100);
/**
* @param unit
* The host to connect to. Give a reachable hostname or IP address, without protocol or port
*/
public MiosUnitConnector(MiosUnit unit, MiosBinding binding) {
logger.debug("Constructor: unit '{}', binding '{}'", unit, binding);
this.unit = unit;
this.binding = binding;
Builder builder = new AsyncHttpClientConfig.Builder();
builder.setRequestTimeoutInMs(unit.getTimeout());
// Use the JDK Provider for now, we're not looking for server-level
// scalability, and we'd like to lighten the load for folks wanting to
// run atop RPi units.
this.client = new AsyncHttpClient(new JDKAsyncHttpProvider(builder.build()));
pollCall = new LongPoll();
pollThread = new Thread(pollCall);
}
/***
* Check if the connection to the MiOS instance is active
*
* @return true if an active connection to the MiOS instance exists, false otherwise
*/
public boolean isConnected() {
return isRunning() && pollCall.isConnected();
}
/**
* Attempts to create a connection to the MiOS host and begin listening for updates over the async HTTP connection.
*
* @throws ExecutionException
* @throws InterruptedException
* @throws IOException
*/
public void open() throws IOException, InterruptedException, ExecutionException {
running = true;
pollThread.start();
}
public void restart() {
pollCall.restart();
}
/***
* Close this connection to the MiOS instance
*/
public void close() {
running = false;
}
/**
* Is the underlying Polling thread running?
*
* @return true if it's running.
*/
public boolean isRunning() {
return running;
}
private static String toBindValue(String value, Command command, State state) {
// TODO: Allow for more complex Bind expressions, to allow for different
// increment/decrement values, and various other transformations that
// may be required.
// Perform a simple item-value substitution on the resulting string.
if (value == null) {
return state.toString();
} else if (value.contains(BIND_COMMAND_VALUE)) {
return value.replace(BIND_COMMAND_VALUE, command.toString());
} else if (value.contains(BIND_ITEM_INCREMENT)) {
String tmp = String.valueOf(Integer.parseInt(state.toString()) + 1);
return value.replace(BIND_ITEM_INCREMENT, tmp);
} else if (value.contains(BIND_ITEM_DECREMENT)) {
String tmp = String.valueOf(Integer.parseInt(state.toString()) - 1);
return value.replace(BIND_ITEM_DECREMENT, tmp);
} else if (value.contains(BIND_ITEM_VALUE)) {
return value.replace(BIND_ITEM_VALUE, state.toString());
} else {
return value;
}
}
private void callDevice(DeviceBindingConfig config, Command command, State state) throws TransformationException {
logger.debug("callDevice: Need to remote-invoke Device '{}' action '{}' and current state '{}')",
new Object[] { config.toProperty(), command, state });
String newCommand = config.transformCommand(command);
if (newCommand == null) {
logger.warn("callDevice: Command '{}' not supported, or missing command: mapping, for Item '{}'",
command.toString(), config.getItemName());
return;
} else if (newCommand.equals("")) {
logger.trace("callDevice: Item '{}' has disabled the use of Command '{}' via its configuration '{}'",
new Object[] { config.getItemName(), command.toString(), config.toProperty() });
return;
}
Matcher matcher = DEVICE_PATTERN.matcher(newCommand);
if (matcher.matches()) {
try {
MiosUnit u = getMiosUnit();
String serviceName = DeviceBindingConfig.mapServiceAlias(matcher.group("serviceName"));
String serviceAction = matcher.group("serviceAction");
String serviceParam = matcher.group("serviceParam");
String serviceValue = matcher.group("serviceValue");
logger.debug(
"callDevice: decoded as serviceName '{}' serviceAction '{}' serviceParam '{}' serviceValue '{}'",
new Object[] { serviceName, serviceAction, serviceParam, serviceValue });
// Perform any necessary bind-variable style transformations on
// the value, before we put it into the URL.
serviceValue = toBindValue(serviceValue, command, state);
// If the parameters to the URL are specified, then we need to
// build the parameter section of the URL, encoding parameter
// names and values... trust no-one 8)
if (serviceParam != null) {
String p = URLEncoder.encode(serviceParam, ENCODING_CHARSET) + '='
+ URLEncoder.encode(serviceValue, ENCODING_CHARSET);
callMios(String.format(DEVICE_URL_PARAMS, u.getHostname(), u.getPort(), config.getMiosId(),
URLEncoder.encode(serviceName, ENCODING_CHARSET),
URLEncoder.encode(serviceAction, ENCODING_CHARSET), p));
} else {
callMios(String.format(DEVICE_URL, u.getHostname(), u.getPort(), config.getMiosId(),
URLEncoder.encode(serviceName, ENCODING_CHARSET),
URLEncoder.encode(serviceAction, ENCODING_CHARSET)));
}
} catch (UnsupportedEncodingException uee) {
logger.debug("Really, trust me, this won't happen ;) exception='{}'", uee);
}
} else {
logger.error("callDevice: The parameter is in the wrong format. BindingConfig '{}', UPnP Action '{}'",
config, newCommand);
}
}
private void callScene(SceneBindingConfig config, Command command, State state) throws TransformationException {
logger.debug("callScene: Need to remote-invoke Scene '{}'", config.toProperty());
String newCommand = config.transformCommand(command);
if (newCommand != null) {
MiosUnit u = getMiosUnit();
callMios(String.format(SCENE_URL, u.getHostname(), u.getPort(), config.getMiosId()));
} else {
logger.warn("callScene: Command '{}' not supported, or missing command: declaration, for Item '{}'",
command.toString(), config.getItemName());
}
}
private void callMios(String url) {
logger.debug("callMios: Would like to fire off the URL '{}'", url);
try {
@SuppressWarnings("unused")
Future<Integer> f = getAsyncHttpClient().prepareGet(url).execute(new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
// Yes, their content-type really does come back with just "xml", but we'll add "text/xml" for
// when/if they ever fix that bug...
if (!(response.getStatusCode() == 200 && ("text/xml".equals(response.getContentType())
|| "xml".equals(response.getContentType())))) {
logger.debug("callMios: Error in HTTP Response code {}, content-type {}:\n{}",
new Object[] { response.getStatusCode(), response.getContentType(),
response.hasResponseBody() ? response.getResponseBody() : "No Body" });
}
return response.getStatusCode();
}
@Override
public void onThrowable(Throwable t) {
logger.warn("callMios: Exception Throwable occurred fetching content: {}", t.getMessage(), t);
}
}
);
// TODO: Run it and walk away?
//
// Work out a better model to gather information about the
// success/fail of the call, log details (etc) so things can be
// diagnosed.
} catch (Exception e) {
logger.warn("callMios: Exception Error occurred fetching content: {}", e.getMessage(), e);
}
}
/**
* Request that a MiOS Scene be triggered.
*
* Used by openHAB Action code, this method fires off the MiOS Scene associated with the Scene Item.
*
* @param config
* A Scene [Item] Binding Configuration.
*/
public void invokeScene(SceneBindingConfig config) {
logger.debug("invokeScene: Invoking remote Scene '{}'", config.toProperty());
MiosUnit u = getMiosUnit();
callMios(String.format(SCENE_URL, u.getHostname(), u.getPort(), config.getMiosId()));
}
/**
* Request that a MiOS Device Action be triggered.
*
* Used by openHAB Action code, this method fires off the MiOS Action associated with the Device Item.
*
* @param config
* A Device [Item] Binding Configuration.
* @param actionName
* a UPnP-style Action to fire off on the remote MiOS Unit.
* @param params
* a NV-Pair style list of parameters to be used by the Action call.
*/
public void invokeAction(DeviceBindingConfig config, String actionName, List<Entry<String, Object>> params) {
Matcher matcher = ACTION_PATTERN.matcher(actionName);
if (matcher.matches()) {
try {
MiosUnit u = getMiosUnit();
String serviceName = DeviceBindingConfig.mapServiceAlias(matcher.group("serviceName"));
String serviceAction = matcher.group("serviceAction");
if (params != null) {
String p = "";
for (Entry<String, Object> entry : params) {
if (p.length() != 0) {
p += '&';
}
p += URLEncoder.encode(entry.getKey(), ENCODING_CHARSET) + '='
+ URLEncoder.encode(entry.getValue().toString(), ENCODING_CHARSET);
}
callMios(String.format(DEVICE_URL_PARAMS, u.getHostname(), u.getPort(), config.getMiosId(),
URLEncoder.encode(serviceName, ENCODING_CHARSET),
URLEncoder.encode(serviceAction, ENCODING_CHARSET), p));
} else {
callMios(String.format(DEVICE_URL, u.getHostname(), u.getPort(), config.getMiosId(),
URLEncoder.encode(serviceName, ENCODING_CHARSET),
URLEncoder.encode(serviceAction, ENCODING_CHARSET)));
}
} catch (UnsupportedEncodingException uee) {
logger.debug("Really, trust me, this won't happen ;) exception='{}'", uee);
}
}
}
/**
* Request a Command be delivered to the MiOS Unit under control.
* <p>
* The MiOS Binding uses this call to deliver "single valued" openHAB Commands to the target MiOS Unit.
* <p>
* Configurable transformations, defined at the BindingConfig level, are used to transform this openHAB Command into
* the specific form required by the MiOS Unit.
* <p>
* openHAB Commands can only be targeted to Device and Scene Bindings, all other requests will cause a warning to be
* logged.
*/
public void invokeCommand(MiosBindingConfig config, Command command, State state) throws Exception {
if (config instanceof SceneBindingConfig) {
callScene((SceneBindingConfig) config, command, state);
} else if (config instanceof DeviceBindingConfig) {
callDevice((DeviceBindingConfig) config, command, state);
} else {
logger.warn("Unhandled command execution for Command ('{}') on binding '{}'", command, config);
}
}
private MiosUnit getMiosUnit() {
return unit;
}
private MiosBinding getMiosBinding() {
return binding;
}
private AsyncHttpClient getAsyncHttpClient() {
return client;
}
/**
* MiOS Poll code.
*
* This code will stand up a thread to serially poll the target MiOS Unit. The initial poll request will return the
* full content from the MiOS unit. Subsequent calls are requested to only return the incremental/changed contents.
*
* If a processing error (Timeout, etc) is detected, then a full content poll is again initiated (since things can
* change during the interval)
*
* Upon successful polling, a series of calls are made to openHAB to push the data to the respective bindings, so
* that data values will change within the user's Rules (etc).
*
* @author Mark Clark
* @since 1.6.0
*/
private class LongPoll implements Runnable {
private boolean connected = false;
private Integer loadTime = null;
private Integer dataVersion = null;
private int failures = 0;
private final ObjectMapper mapper = new ObjectMapper();
private static final String BASE_URL = "http://%s:%d/data_request";
private static final String STATUS2_URL = BASE_URL + "?id=status2";
private static final String STATUS2_INCREMENTAL_URL = STATUS2_URL
+ "&LoadTime=%d&DataVersion=%d&Timeout=%d&MinimumDelay=%d";
public LongPoll() {
}
@SuppressWarnings("unchecked")
private List<Object> getList(Map<String, Object> data, String param) {
if (!data.containsKey(param)) {
return new ArrayList<Object>();
}
return (List<Object>) data.get(param);
}
private String getUri(boolean incremental) {
MiosUnit unit = getMiosUnit();
if (incremental) {
AsyncHttpClientConfig c = getAsyncHttpClient().getConfig();
// Use a timeout on the MiOS URL call that's about 2/3 of what
// the connection timeout is.
int t = Math.min(c.getIdleConnectionTimeoutInMs(), unit.getTimeout()) / 500 / 3;
int d = unit.getMinimumDelay();
return String.format(Locale.US, STATUS2_INCREMENTAL_URL, unit.getHostname(), unit.getPort(), loadTime,
dataVersion, new Integer(t), new Integer(d));
} else {
return String.format(Locale.US, STATUS2_URL, unit.getHostname(), unit.getPort());
}
}
private Object fixTimestamp(Object value) {
if (value == null) {
return value;
} else if (value instanceof Integer) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(((Integer) value).intValue() * 1000l);
return cal;
} else if (value instanceof String && !value.equals("")) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(Integer.parseInt((String) value) * 1000l);
return cal;
} else {
return value;
}
}
private void publish(String property, Object value, boolean incremental) {
String p = getMiosUnit().formatProperty(property);
try {
getMiosBinding().postPropertyUpdate(p, value, incremental);
} catch (Exception e) {
logger.error("Exception '{}' raised pushing property '{}' value '{}' into openHAB",
new Object[] { e.getMessage(), p, value, e });
}
}
private void processSystem(Map<String, Object> system, boolean incremental) {
for (Map.Entry<String, Object> entry : system.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (!key.equals("devices") && !key.equals("scenes") && !key.equals("rooms")
&& !key.equals("sections")) {
if (value instanceof String || value instanceof Integer || value instanceof Double
|| value instanceof Boolean) {
// Skip over Lua code blocks and
// fix [known] date-time values on the way through, so
// they use the native type.
if (key.equals("DeviceSync") || key.equals("LoadTime") || key.equals("zwave_heal")
|| key.equals("TimeStamp")) {
value = fixTimestamp(value);
}
String property = "system:/" + key;
publish(property, value, incremental);
} else {
// No need to bring these into openHAB, they'll only
// bulk up
// the system.
if (key.equals("StartupCode") || key.equals("startup")) {
continue;
}
logger.debug("processSystem: skipping key={}, class={}", key, value.getClass());
}
}
}
}
private void processDevices(List<Object> devices, boolean incremental) {
for (Object d : devices) {
@SuppressWarnings("unchecked")
Map<String, Object> device = (Map<String, Object>) d;
// Note that the "name" field is not present in status2
// responses,
// like it is in user_data2 responses.
//
// These can be either an Integer or a String, either way it's
// an int. Newer ones tend to be Strings, so we'll convert them
// all to String values.
//
String deviceId;
Object v = device.get("id");
if (v instanceof String) {
deviceId = (String) v;
} else if (v instanceof Integer) {
deviceId = ((Integer) v).toString();
} else {
throw new IllegalArgumentException("WTF?");
}
// Enumerate Device Attributes
for (Entry<String, Object> da : device.entrySet()) {
String key = da.getKey();
Object value = da.getValue();
if (value instanceof String || value instanceof Integer || value instanceof Double
|| value instanceof Boolean) {
if (key.equals("time_created")) {
// fix [known] date-time values on the way through,
// so they use the native type.
value = fixTimestamp(value);
} else if (key.equals("id")) {
// Skip "id", since it's done at the end.
continue;
}
boolean force = false;
boolean statusAttr = key.equals("status");
// Handle a bug in MiOS's JSON, where they send the STATUS Attribute even if it hasn't
// changed. This resulted in a lot of unnecessary writes to openHAB. We can do this
// because this thread is the single-source of values for the STATUS attribute.
if (statusAttr) {
Integer lastValue = deviceStatusCache.get(deviceId);
if (!value.equals(lastValue)) {
deviceStatusCache.put(deviceId, (Integer) value);
force = true;
}
}
if (!statusAttr || force) {
String property = "device:" + deviceId + '/' + key;
publish(property, value, incremental);
}
} else {
// No need to bring these into openHAB, they'll only
// bulk up
// the system.
if (key.equals("states") || key.equals("ControlURLs") || key.equals("Jobs")
|| key.equals("tooltip")) {
continue;
}
logger.debug("processDevices: skipping key={}, class={}", key, value.getClass());
}
}
// Enumerate Device State Variables
List<Object> deviceStates = getList(device, "states");
boolean variablesProcessed = false;
for (Object ds : deviceStates) {
@SuppressWarnings("unchecked")
Map<String, Object> state = (Map<String, Object>) ds;
String var = (String) state.get("service") + "/" + (String) state.get("variable");
String property = "device:" + deviceId + "/service/" + var;
// Can be String or Integer
Object value = state.get("value");
publish(property, value, incremental);
variablesProcessed = true;
}
// The Device's ID attribute is last in the list and only
// sent when at-least-one UPnP State Variable has been changed.
//
// This is done to allow folks something to "trigger" off in a
// rule if they have cross-Item validation dependencies.
//
// Otherwise they'll come out in JSON order
// (unpredictable) which may not be the order they're
// normally changed at the MiOS end. Making this last
// would be like having an "end of [device] transaction"
// marker.
if (variablesProcessed) {
publish("device:" + deviceId + "/id", deviceId, incremental);
}
}
}
private void processScenes(List<Object> scenes, boolean incremental) {
for (Object s : scenes) {
@SuppressWarnings("unchecked")
Map<String, Object> scene = (Map<String, Object>) s;
// Note that the "name" field is not present in status2
// responses,
// like it is in user_data2 responses.
// These can be either an Integer or a String, either way it's
// an int. Newer ones tend to be Strings, so we'll convert them
// all to String values.
Integer sceneId = (Integer) scene.get("id");
// Enumerate Scene Attributes
for (Entry<String, Object> da : scene.entrySet()) {
String key = da.getKey();
Object value = da.getValue();
if (value instanceof String || value instanceof Integer || value instanceof Double
|| value instanceof Boolean) {
if (key.equals("Timestamp")) {
// fix [known] date-time values on the way through,
// so they use the native type.
value = fixTimestamp(value);
// Fix the key so it's consistent with the one used
// at the System level.
// TODO: Document this.
key = "TimeStamp";
}
boolean force = false;
boolean statusAttr = key.equals("status");
// Handle a bug in MiOS's JSON, where they send the STATUS Attribute even if it hasn't
// changed. This resulted in a lot of unnecessary writes to openHAB. We can do this
// because this thread is the single-source of values for the STATUS attribute.
if (statusAttr) {
Integer lastValue = sceneStatusCache.get(sceneId);
if (!value.equals(lastValue)) {
sceneStatusCache.put(sceneId, (Integer) value);
force = true;
}
}
if (!statusAttr || force) {
String property = "scene:" + sceneId + "/" + key;
publish(property, value, incremental);
}
} else {
// No need to bring these into openHAB, they'll only
// bulk up
// the system.
if (key.equals("timers") || key.equals("triggers") || key.equals("groups")
|| key.equals("tooltip") || key.equals("Jobs") || key.equals("lua")) {
continue;
}
logger.debug("processScenes: skipping key={}, class={}", key, value.getClass());
}
}
}
}
private void processRooms(List<Object> rooms, boolean incremental) {
// TODO: Implement
}
private void processSections(List<Object> sections, boolean incremental) {
// TODO: Implement
}
private void processResponse(Map<String, Object> response, boolean incremental) {
Integer lt = (Integer) response.get("LoadTime");
Integer dv = (Integer) response.get("DataVersion");
if (lt != null && dv != null) {
connected = true;
List<Object> devices = getList(response, "devices");
List<Object> scenes = getList(response, "scenes");
List<Object> rooms = getList(response, "rooms");
List<Object> sections = getList(response, "sections");
logger.debug(
"processResponse: success! loadTime={}, dataVersion={} devices({}) scenes({}) rooms({}) sections({})",
new Object[] { lt, dv, Integer.toString(devices.size()), Integer.toString(scenes.size()),
Integer.toString(rooms.size()), Integer.toString(sections.size()) });
processDevices(devices, incremental);
processScenes(scenes, incremental);
processRooms(rooms, incremental);
processSections(sections, incremental);
processSystem(response, incremental);
// Only reset these once we've successfully loaded/parsed the
// content.
this.loadTime = lt;
this.dataVersion = dv;
this.failures = 0;
} else {
throw new RuntimeException("Processing error on MiOS JSON Response - malformed");
}
}
public boolean isConnected() {
return connected;
}
// TODO: Need to make this stream-oriented, so we don't have to keep the
// entire (large) MiOS JSON string in memory at the same time as the
// parsed JSON result.
@SuppressWarnings("unchecked")
private Map<String, Object> readJson(String json) {
if (json == null) {
return new HashMap<String, Object>();
}
try {
return mapper.readValue(json, Map.class);
} catch (JsonParseException e) {
// TODO: Replace RuntimeException with a more specialized
// exception.
throw new RuntimeException("Failed to parse JSON", e);
} catch (JsonMappingException e) {
// TODO: Replace RuntimeException with a more specialized
// exception.
throw new RuntimeException("Failed to map JSON", e);
} catch (IOException e) {
// TODO: Replace RuntimeException with a more specialized
// exception.
throw new RuntimeException("Failed to read JSON", e);
}
}
private boolean fullReload;
private long lastFullReload;
private Object fullReloadLock = new Object();
public void restart() {
synchronized (this.fullReloadLock) {
this.lastFullReload = System.currentTimeMillis();
this.fullReload = true;
}
logger.debug("restart: Resetting, requesting forced reload. lastFullReload={}", this.lastFullReload);
}
@Override
public void run() {
restart();
MiosUnit unit = getMiosUnit();
int startupDelay = unit.getStartupDelay();
int errorCount = unit.getErrorCount();
long loopCount = 0l;
do {
try {
//
// On the first loop, and each time we're restarted through openHAB config change, add
// a delay to bundle the incoming openHAB Configuration events.
//
while (this.fullReload) {
int sleepTime;
synchronized (this.fullReloadLock) {
sleepTime = (int)(this.lastFullReload + startupDelay - System.currentTimeMillis());
if (sleepTime <= 0) {
this.fullReload = false;
}
}
if (sleepTime <= 0) {
loopCount = 0l;
logger.debug("run: finishing sleep cycle.");
break;
}
logger.debug("run: sleeping, delaying reload sleepTime={}", sleepTime);
Thread.sleep(sleepTime);
}
// Force a full poll of the dataSet every time the MiOS Unit
// configuration indicates to do so, or if we get an error.
boolean force = this.fullReload || (errorCount != 0) && (failures != 0) && ((failures % errorCount) == 0);
boolean incremental = (!force && loadTime != null && dataVersion != null);
String uri = getUri(incremental);
logger.debug("run: URI Built was '{}' loop '{}'", uri, loopCount);
Future<Response> f = getAsyncHttpClient().prepareGet(uri).execute();
Response r = f.get();
// Force the Response Charset to be UTF-8, since MiOS isn't setting
// anything in their HTTP response Header.
Map<String, Object> json = readJson(r.getResponseBody("UTF-8"));
if (json.containsKey("error")) {
throw new IOException(json.get("error").toString());
}
connected = true;
processResponse(json, incremental);
// Reset the Loop count only once we've successfully
// processed the Response. Otherwise there's a potential for
// it to never perform a full (initial) fetch call.
int c = getMiosUnit().getRefreshCount();
loopCount = loopCount + 1;
if (c != 0) {
loopCount = (loopCount % c);
}
} catch (InterruptedException ie) {
logger.debug("run: Thread shutdown by Interrupted");
} catch (Exception e) {
connected = false;
this.failures++;
logger.debug(
"run: Exception Error occurred fetching/processing content: {},{}. Total failures ({})",
new Object[] { e.getMessage(), e, Integer.toString(failures) });
// TODO: Make the pause configurable and/or add a backoff
// mechanism.
//
// Pause a little before restarting, to give things time to
// recover.
try {
Thread.sleep(5000);
} catch (InterruptedException tex) {
}
}
} while (isRunning());
logger.info("run: Shutting down MiOS Polling thread. Unit={}", getMiosUnit().getName());
connected = false;
}
}
}